js基础知识整理(编译、作用域,词法作用域,闭包,执行上下文,GC,this,继承等,万字总结)(二)
this
在js代码编译阶段,会创建执行上下文,又会创建当前上下文的词法环境和变量环境,this就是词法环境记录的一个属性。this是在运行时(函数被调用时)绑定的,不是在编译/编写时候。this和函数声明位置没有关系,只和函数调用位置有关系,它指向哪里完全取决函数在哪里被调用。这点和词法作用域相反,词法作用域和函数调用方式无关,和声明位置有关。
调用方式和执行上下文栈
前面说this只和函数调用方式有关系,什么是调用方式?就是函数在代码中调用的方式(听君一席话,全部是废话)调用方式还要从执行上下文和执行上下文栈说起。当我们调用任何函数时候,这个函数引用有执行上下文对象,this就会绑定在这个执行上下文,多层引用只有最后一层有效。
4种this绑定方式
默认绑定
以下代码中foo调用,是不带任何修饰符的调用,this默认绑定,指向全局。
1 | |
隐式绑定(上下文调用)
当调用位置有上下文对象时候,比如以下f函数调用方式,是通过obj的上下文来引用的,隐式绑定会把函数中的this绑定到这个上下文对象。函数被调用时,obj对象拥有它,因此this指向obj
1 | |
当具有多层引用时候,只有最后一层有效
1 | |
隐式绑定丢失的情况,bar函数引用的是f函数本身,所以调用时bar()时候相当于不带任何修饰,会应用默认绑定
1 | |
函数作为参数,也会发生隐式绑定丢失,因为参数传递也是一种赋值
1 | |
在setTimeout,setInterval等内置js函数中,也会发生隐式绑定丢失
1 | |
显式绑定(call,apply,bind)
如果我们想将函数调用强制指向某个对象,应该怎么做?可以采用call和apply,这两个api的作用相同,唯一的不同就是传参方式不同,两者性能差异基本可以忽略,如果非要说性能的话,call会比apply好一点,ECMA官网的介绍中,apply会比call算法上多一个CreateListFromArrayLike的步骤。除了这两种以外,还有一个ES5的内置函数,bind方法
call(Obj,params1,params2,...)会立即调用并且执行apply(Obj,[params1,params2,...])会立即调用并且执行Function.prototype.bind(Obj)不会立即调用,返回新函数除此之外,1
2
3
4
5
6
7
8
9
10
11let obj = {
f(){
console.log(this) // obj
}
}
var bar = obj.f //把f函数本身给了bar
bar.call(obj) // 通过call修改this指向,所以this是obj,会立即执行
bar.apply(obj) // apply调用也一样,会立即执行
var newBar = bar.bind(obj) // 修改this之后会返回一个新的函数,不会立即执行
newBar()js和宿主环境的内置函数,都有一个可选参数,来确保回调函数使用指定的this,比较常见的就是forEach和map。这两个函数最后都有一个参数,用来确保当前回调函数使用指定的thisArray.prototype.forEach(fn,[thisArg])Array.prototype.map(fn,[thisArg])这里提到了循环的1
2
3
4
5
6
7
8
9
10function foo(el) {
console.log(el,this.id)
};
var obj = {
id:"awesome"
};
[1,2,3].forEach(foo,obj) // awesome 会输出三次,因为将this显式绑定在了obj
[1,2,3].map(foo,obj) // map也相同forEach和map,顺带说一下循环效率问题,在我之前测试中,for循环速度最快,其次是forEach,最后才是mapnew绑定
在介绍new绑定之前,先说一下new操作符。- 在
js中,构造函数只是使用new操作符被调用的函数。它们不属于某个类,也不会实例化一个类。 - 构造函数也不是特殊的函数类型,只是被
new操作符调用的普通函数。 - 不存在构造函数,只有构造调用
以上介绍来自你不知道的javascript。说白了,在js中,不存在构造函数,只有new操作符调用的普通函数,也就是构造调用。
那么,当我们使用new操作符来调用函数时候,或者说发生构造调用,和我们普通调用函数有什么区别呢?
1.创造一个全新的对象
2.这个新的对象会执行[[Prototype]]链接
3.新对象会绑定到函数调用的this
4.如果这个函数没有其他返回对象(意思是用户的函数返回的不是一个对象,而是一个普通的值,那么就丢弃掉),那么new表达式中的函数调用会返回这个新的对象
实际上,在ECMAScript规范中,需要的步骤会更多,以上只是简化版本。简单来讲,通过new调用,会创建一个新的对象,并且将用户的函数this绑定到这个对象上,执行函数返回。
当我们知道了以上这几点时候,我们可以来尝试一下自己实现一个new函数
1 | |
例外情况:箭头函数绑定
箭头函数是es6中的语法
- 箭头函数没有自己的
this,arguments,super等 - 箭头函数没有
prototype - 箭头函数不可以使用
new调用 - 箭头函数的
this绑定无法被修改
什么叫做没有自己的this,arguments,super呢?也就是箭头函数中的this并不是自己的this,而是根据外层作用域来决定的,也可以说是根据当前的词法作用域来决定的。来看以下的例子
1 | |
其实以上代码相当于
1 | |
总结this绑定,一共有4种绑定
- 默认调用。绑定到全局对象
- 隐式调用(上下文调用),绑定到那个上下文对象
- 显式调用(call,apply,bind),绑定到指定对象
new调用,绑定到新创建的对象- 例外情况:箭头函数,词法作用域决定
this,绑定到外层作用域
原型
__proto__或者叫([[Prototype]])
js中所有的对象(函数也是对象)都有一个__proto__内置属性,是一个对象,就是对于其他对象的引用,是隐式原型。和[[Prototype]]意义相同,注意是[[Prototype]]不是prototype
1 | |
原型链
简单一点说,就是在访问对象属性,实际上就是在执行[[Get]](),按照查找规则就是
- 查找自身,自身没有查找原型
- 原型对象也是对象,也会被执行
[[Get]]()操作 - 最终尽头是
Object.prototype.__proto__,值为null
这样不断查找关联起来的就叫做原型链,Object.prototype有很多通用方法都在上面,比如toString(),valueOf()
prototype
prototype是每个函数都有的公开属性,且不可枚举,指向另外一个对象,是显式原型
1 | |
constructor
constructor是prototype公有并且不可枚举的属性,这个属性引用是这个函数的本身
1 | |
prototype和__proto__、constructor之间的关系
1 | |
- 1.为什么以上代码的
foo.__proto__和Foo.prototype全等?回想之前的this绑定中的new绑定执行的操作,在第三步时候,将新创建的对象的__proto__指向函数的prototype,最后返回这个对象。所以foo.__proto__ === Foo.prototype1
2
3
4
5
6
7
8function newn() {
...
// 3.修改prototype
obj.__proto__ = fn.prototype
...
// 5.如果用户函数本来有返回值,并且是一个对象就返回这个对象,否则返回新创建的对象
return result instanceof Object ? result : obj
} - 2.为什么
foo.constructor === Foo?同样,也是因为在new的时候,执行了第三步,也就是所以,当查找1
2
3
4
5
6obj.__proto__ = fn.prototype
// fn.prototype就是
fn.prototype = {
constructor:fn()
}
最后赋值给obj.__proto__foo.constructor,并不是自身的constructor,根据对象查找规则,自身没有就会查找__proto__,这里就是查找的__proto__上的constructor。
最后画一张图
函数和对象的关系
- 1.js内置对象本质上是函数,通过
new调用,就是构造函数。所有函数都继承于Function。其实可以看到,本质上都是函数,只要是函数,都继承于1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
对应就是
ƒ String() { [native code] }
ƒ Number() { [native code] }
ƒ Boolean() { [native code] }
ƒ Object() { [native code] }
ƒ Function() { [native code] }
ƒ Array() { [native code] }
ƒ Date() { [native code] }
ƒ RegExp() { [native code] }
ƒ Error() { [native code] }Function,包括Function本身和Object1
2
3Function.__proto__ === Function.prototype // true
Number.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true - 2.所有的对象都继承于
Object,函数也是对象,所有对象原型链终点是Object.prototype1
2
3
4
5
6
7
8
9
10var str = new String('i am string')
str instanceof Object // true
str instanceof String // true
// 原型链表示为
str → String.prototype → Object.prototype → null
function f(){}
f.__proto__ === Function.prototype // true
// 原型链表示为
f → === Function.prototype → Object.prototype → null所以对象都包含Object原型上的方法,Number,String,Booelan是包装对象,也是对象1
2
3
4
5
6
7为什么?因为Object函数继承于Function,所以Object.__proto__ === Function.prototype
Object instanceof Function // true
为什么?因为Function也继承于Function,所以Function.__proto__ === Function.prototype,Function.prototype的__proto__指向Object.prototype
Function instanceof Object // true
原型小结
- 每个对象都有一个原型对象
__proto__,访问对象属性会触发内部的[[Get]]()函数,先查找自身,在查找__proto__中指向的对象,指向的对象也是对象,这样不断查找,构成了原型链 __proto__是每个对象都有的属性,隐式原型prototype是每个函数都有的属性,显式原型constructor是prototype的一个属性,指向其函数自身
继承
原型链继承
这种继承方式有两个最大的问题
- 1.多个子类实例化时候,修改了父类,父类属性会被共享
- 2.无法在不影响父类情况下,给函数传递参数问:如果我直接通过这种写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31function Super () {
this.property = true
this.arr = []
}
Super.prototype.getSuperValue = function () {
return this.property
}
Super.prototype.setPush = function () {
this.arr.push(1)
}
Super.prototype.getArr = function () {
return this.arr
}
function Sub () {
this.subProperty = false
}
// 相当于Sub.prototype = Super.prototype
// 原型继承的关键
Sub.prototype = new Super()
Sub.prototype.getSubValue = function () {
return this.subproperty;
};
// 所以此时,Sub会拥有其父类的方法
let instance = new Sub()
let instance1 = new Sub()
// 这里调用的就是.__proto__上面父类的方法
console.log(instance.getSuperValue()) // true
// 修改其中一个子类
console.log(instance.setPush())
// 只要一个子类修改了父级属性,所有子类都会被共享
console.log(instance1.getArr()) // [1]Sub.prototype = Super.prototype,直接将原型关联起来,可不可以?
当然是不可以,这样的话就是直接引用了父类的prototype,修改子类会导致父类的prototype也发生变化,通过new关键字,就可以创建新的对象,将.__proto__指向父类的prototype
借用构造函数
- 这种继承方式,虽然解决了原型链继承不能传递参数问题,但是方法要在构造函数中定义。
- 还有子类无法访问父类的方法
1
2
3
4
5
6
7
8
9
10
11
12
13// 借用构造函数
function Super (name) {
this.name = name
}
function Sub () {
// 这里通过call执行了父类方法,this指向子类,相当于子类也有name属性
// 继承并且传参
Super.call(this,"John")
this.age = 20
}
const instance = new Sub()
// 此时instance会有age和name两个属性
console.log(instance) // {name: "John", age: 20}组合继承
组合继承是原型链继承和构造函数继承两种方法的结合。 - 原型链继承方法
- 借用构造函数继承属性
这种方式弥补了原型链继承和构造函数继承的缺点可以创建多个互不影响的实例,子类有自己的属性和方法,也可以共享父类属性不过这种方法也有问题,就是父类构造函数始终会被调用两次1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23function Super(name) {
// 定义父类实例属性
this.name = name
this.age = 18
}
// 定义父类实例方法
Super.prototype.sayName = function () {
console.log(this.name)
}
function Sub(name) {
// 借用构造函数继承父类实例属性
// 父类第二次被调用
Super.call(this, name)
}
// 原型链继承父类实例方法
// 父类第一次被调用
Sub.prototype = new Super()
const instance = new Sub('John')
console.log(instance, 'instance') // {name: "John", age: 18}
const instance2 = new Sub('Bob')
console.log(instance2, 'instance') // {name: "Bob", age: 18}原型式继承
其实就是Object.create()方法的polyfill实现方法。ES5引入了Object.create()。
根据红宝书第四版介绍,使用场景为一般在你想基于一个对象创建另外一个对象时候,可以使用原型式继承也就是Object.create()
1 | |
其实看上去可能有点迷惑,不过我们只需要知道,这样做的最终目的无非两点
- 1.返回新对象实例
- 2.新对象实例的.__proto__指向我们传入的对象,这样就可以调用和共享我们之前的方法和属性,也可以拓展我们现有对象自己的属性和方法
寄生式继承
如果我们只想关注对象,不关注方法,还可以通过寄生式继承来实现,寄生式继承基于原型式继承实现
1 | |
寄生组合式继承
这种方式可以解决组合式继承多次调用父类函数的问题
1 | |
垃圾回收
参考写的另外的一篇垃圾回收文章
V8中的垃圾回收器与内存管理
参考资料:
https://juejin.cn/post/6844903953981767688 JavaScript:V8编译过程
https://juejin.cn/post/6961349015346610184 Event Loop 和 JS
引擎、渲染引擎的关系
https://www.cnblogs.com/wangfupeng1988/p/3977924.html 深入理解javascript原型和闭包(完结)
https://blog.csdn.net/szengtal/article/details/78726143 词法环境:ECMA实现(一)— 简介
https://juejin.cn/post/6926831181681917959#heading-6 详解ES6执行时词法环境/作用域/执行上下文/执行栈和闭包
https://262.ecma-international.org ECMA原文地址
本文标题:js基础知识整理(编译、作用域,词法作用域,闭包,执行上下文,GC,this,继承等,万字总结)(二)
文章作者:Niuhk
发布时间:2021-07-22
最后更新:2022-01-23
原始链接:https://www.niuhk.cn/2021/07/22/js基础知识整理(编译、作用域,词法作用域,闭包,执行上下文,GC,this,继承等,万字总结)(二) /
版权声明:转载请注明出处!
分享